﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.Reflection;
using System.Web;
using System.Web.UI;
using System.Text;

using BizElements.BusinessLayer;

namespace BizElements.Web
{
    #region Documentation.
    /// <summary>
    /// Wraps a unit of business logic in a <see cref="IWebCommand"/> object for abstract/base and concrete/derived types.
    /// </summary>
    /// <typeparam name="TBizObject">Base class which contains properties to which command parameters
    /// (see <see cref="ICommand.RequiredParameters"/>) are mapped and which defines methods that
    /// implement domain logic and horizontal security. The concrete type derived from the base class
    /// are instantiated by invoking a static factory method.</typeparam>
    /// <typeparam name="TFactory">Factory class.</typeparam>
    /// <remarks><para><b>BusinessObjectFactoryCommand</b> uses a factory class to initialize an object before it 
    /// populates it's properties with required parameter values.</para>
    /// <para><b>BusinessObjectFactoryCommand</b> supports multiple records. The <b>Execute</b> methods loops 
    /// until all records have been processed.</para>
    /// <para>Default UI culture provider for the current user may be configured in the <b>appSettings</b> section 
    /// of a *.config file. You can override default configuration by <see cref="SetMessageSourceCultureProvider"/> 
    /// method. Example:
    /// <code>
    /// <add key="BizElements.Web.BusinessObjectCommand.DefaultUiCultureProviderAssembly" value="BizElements.Web"/>
    /// <add key="BizElements.Web.BusinessObjectCommand.DefaultUiCultureProviderClass" value="BizElements.Web.CommonGetters"/>
    /// <add key="BizElements.Web.BusinessObjectCommand.DefaultUiCultureProviderMethod" value="GetUiCulture"/>
    /// </code>
    /// </para></remarks>
    #endregion
    public class BusinessObjectFactoryCommand<TBizObject, TFactory> : WebCommandBase
        where TBizObject : class, IBusinessObject
        where TFactory : IBusinessObjectFactory<TBizObject>, new()
    {
        #region Configuration - isti config keys kao i BusinessObjectCommand.

        /// <summary>
        /// Gets default UI culture provider configured in the config file.
        /// </summary>
        static readonly Delegates.Function<CultureInfo> DefaultUiCultureProvider;

        static BusinessObjectFactoryCommand()
        {
            string cultureGetterAsm = ConfigurationManager.AppSettings["BizElements.Web.BusinessObjectCommand.DefaultUiCultureProviderAssembly"];
            string cultureGetterClass = ConfigurationManager.AppSettings["BizElements.Web.BusinessObjectCommand.DefaultUiCultureProviderClass"];
            string cultureGetterMethod = ConfigurationManager.AppSettings["BizElements.Web.BusinessObjectCommand.DefaultUiCultureProviderMethod"];
            if (!string.IsNullOrEmpty(cultureGetterAsm) && !string.IsNullOrEmpty(cultureGetterClass) && !string.IsNullOrEmpty(cultureGetterMethod))
            {
                Type type = Assembly.Load(cultureGetterAsm).GetType(cultureGetterClass);
                if (type == null)
                    throw new InvalidOperationException("Cannot load the '" + cultureGetterClass + "' class.");

                MethodInfo method = type.GetMethod(cultureGetterMethod, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
                if (method == null)
                    throw new InvalidOperationException("The class '" + cultureGetterClass + "' does not contain a static method '" + cultureGetterMethod + "'.");

                DefaultUiCultureProvider = (Delegates.Function<CultureInfo>)Delegate.CreateDelegate(typeof(Delegates.Function<CultureInfo>), method);
            }
        }

        #endregion

        #region Fields.

        string executeMethod;
        ArrayList additionalExecuteArguments = new ArrayList();
        string grantExecuteMethod;
        IDictionary<string, string> parameterToPropertyMappings = new Dictionary<string, string>();
        bool refreshSearchView;
        Delegates.Function<CultureInfo> messageSourceCultureProvider = DefaultUiCultureProvider;

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of the <b>BusinessObjectFactoryCommand</b> class.
        /// </summary>
        protected BusinessObjectFactoryCommand()
        { 
        }

        /// <summary>
        /// Initializes a new instance of the <b>BusinessObjectFactoryCommand</b> class.
        /// </summary>
        /// <param name="key">Command key. Should be unique in the collection and language neutral.</param>
        /// <param name="text">Command description.</param>
        public BusinessObjectFactoryCommand(string key, string text)
            : base(key, text)
        {
        }

        #endregion

        #region Business object mappings, reflection and build-up.

        /// <summary>Sets the method which implements the domain logic.</summary>
        /// <param name="method">A method which accepts a single <see cref="IActor"/> argument. Null is not allowed.</param>
        public virtual void SetExecuteMethod(Delegates.Procedure<IActor> method)
        {
            if (method == null)
                throw new ArgumentNullException("method");

            this.executeMethod = method.Method.Name;
        }

        /// <summary>Sets the method which implements the domain logic.</summary>
        /// <typeparam name="TAdditionalArg">Type of the second method argument.</typeparam>
        /// <param name="method">A method which accepts <see cref="IActor"/> and specified arguments. Null is not allowed.</param>
        /// <param name="arg">Constant value for the second argument.</param>
        public virtual void SetExecuteMethod<TAdditionalArg>(Delegates.Procedure<IActor, TAdditionalArg> method, TAdditionalArg arg)
        {
            if (method == null)
                throw new ArgumentNullException("method");

            this.executeMethod = method.Method.Name;
            this.additionalExecuteArguments.Add(arg);
        }

        /// <summary>Sets the method which implements optional horizontal security, ie. checks whether the current 
        /// actor has sufficient privileges to execute the domain logic method.</summary>
        /// <param name="method">A method which accepts a single <see cref="IActor"/> argument and returns
        /// a collection of rules which prevent the actor to execute the domain logic method. May be null.</param>
        public virtual void SetGrantExecuteMethod(Delegates.Function<BusinessRuleCollection, IActor> method)
        {
            this.grantExecuteMethod = (method != null) ? method.Method.Name : null;
        }

        /// <summary>Sets the mehod which provides a <see cref="CultureInfo"/> used by the <b>MessageSource</b> of the created business object.</summary>
        /// <param name="method">A method which returns a <see cref="CultureInfo"/> object. May be null.</param>
        /// <remarks><para>Has no affect if the specified business class doesn't implement 
        /// <see cref="IBusinessObject"/> interface.</para>
        /// <para>Recommendation: the method should be <b>static</b>.</para></remarks>
        public virtual void SetMessageSourceCultureProvider(Delegates.Function<CultureInfo> method)
        {
            this.messageSourceCultureProvider = method;
        }

        /// <summary>Clears all parameter-property mappings.</summary>
        public virtual void ClearMappings()
        {
            this.parameterToPropertyMappings.Clear();
        }

        /// <summary>Maps property to the provided data parameter.</summary>
        /// <param name="parameterName">The parameter which must be provided before invoking <see cref="Execute"/>
        /// method. Typically a data key field name.</param>
        /// <param name="bizObjectProperty">The property of a business class which is to receive the value 
        /// of the specified parameter.</param>
        /// <remarks>The provided parameter is automatically added to the <see cref="ICommand.RequiredParameters"/>.</remarks>
        public virtual void MapProperty(string parameterName, string bizObjectProperty)
        {
            this.parameterToPropertyMappings.Add(parameterName, bizObjectProperty);
            MatchRequiredParametersToPropertyMappings();
        }

        private void MatchRequiredParametersToPropertyMappings()
        {
            ICollection<string> parameterNames = this.parameterToPropertyMappings.Keys;
            this.RequiredParameters = new string[parameterNames.Count];
            parameterNames.CopyTo(this.RequiredParameters, 0);
        }

        /// <summary>Maps factory method argumen and business object property to the provided data parameter.</summary>
        /// <param name="parameterName">The parameter required by factory method to decide which concrete object is should create.</param>
        public virtual void MapSubtypeIdParameter(string parameterName)
        { 
            string bizObjectProperty = new TFactory().SubtypeIdPropertyName;
            MapProperty(parameterName, bizObjectProperty);
        }

        private string GetParameterMappedToProperty(string propertyName)
        {
            foreach (KeyValuePair<string, string> paramAndProperty in this.parameterToPropertyMappings)
            {
                if (paramAndProperty.Value == propertyName)
                    return paramAndProperty.Key;
            }

            return null;
        }

        private TBizObject BuildObject(IDictionary<string, object> paramsAndValues)
        {
            TBizObject obj;
            var messageSourceCulture = (this.messageSourceCultureProvider != null) ? this.messageSourceCultureProvider() : UserSession.UiCulture;
            var factory = new TFactory();
            string subtypeIdPropertyName = factory.SubtypeIdPropertyName;
            if (string.IsNullOrEmpty(subtypeIdPropertyName))
            {
                obj = factory.NewBusinessObject(messageSourceCulture, /*no subtpying*/ null);
            }
            else
            {                
                string subtypeIdParameterName = GetParameterMappedToProperty(subtypeIdPropertyName);
                if (string.IsNullOrEmpty(subtypeIdParameterName))
                    throw new InvalidOperationException("Subtype ID data parameter is not mapped to factory method argument. Use MapSubtypeIdParameter method to map it.");

                PropertyInfo subtypeIdPropertyInfo = typeof(TBizObject).GetProperty(subtypeIdPropertyName);
                if (subtypeIdPropertyInfo == null)
                    throw new InvalidOperationException("Cannot find public subtype ID property in the base class. Property name: " + subtypeIdPropertyName);

                string typeIdDataParamName = GetParameterMappedToProperty(subtypeIdParameterName);
                if (string.IsNullOrEmpty(typeIdDataParamName))
                    throw new InvalidOperationException("Data parameter that provides subtype ID was not provided.");

                object typeIdDataParamValue = paramsAndValues[typeIdDataParamName];
                object typeId = Convert.ChangeType(typeIdDataParamValue, subtypeIdPropertyInfo.PropertyType);

                obj = factory.NewBusinessObject(messageSourceCulture, typeId);
            }

            foreach (KeyValuePair<string, string> paramAndProperty in this.parameterToPropertyMappings)
            {
                string parameterName = paramAndProperty.Key;
                string propertyName = paramAndProperty.Value;
                PropertyInfo property = obj.GetType().GetProperty(propertyName);
                object paramValue = paramsAndValues[parameterName];
                object propertyValue = Convert.ChangeType(paramValue, property.PropertyType);
                property.SetValue(obj, propertyValue, null);
            }

            return obj;
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets or sets whether the <see cref="ISearchView"/> component which is using the command 
        /// should be refreshed after the command is executed.
        /// </summary>
        /// <remarks><para>Has no effect if the caller object does not implement ISearchView interface.</para>
        /// <para>Set to <b>true</b> if the command affects the data displayed in the current view.
        /// Eg. it deletes or modifies them. Setting this property to <b>true</b> will ensure that the view
        /// refetches data from the data-source.</para></remarks>
        public virtual bool RefreshSearchView
        {
            get { return this.refreshSearchView; }
            set { this.refreshSearchView = value; }
        }

        #endregion

        #region ValidateParameters.

        /// <summary>
        /// Validates the provided parameters.
        /// </summary>
        /// <exception cref="InvalidOperationException">InvalidOperationException is generated if the parameter 
        /// set provided by the <see cref="ICommand.Parameters"/> property is not valid.</exception>
        /// <remarks><para>Checks that the record count falls within constraints defined by
        /// <see cref="ICommand.MinParameterSetSize"/> and <see cref="ICommand.MaxParameterSetSize"/>
        /// properties and that all <see cref="ICommand.RequiredParameters"/> are set for each record.</para>
        /// <para>Override if additional validation is required.</para></remarks>
        protected virtual void ValidateParameters()
        {
            if (this.Parameters.Count < this.MinParameterSetSize)
                throw new InvalidOperationException("Command requires that at least " + this.MinParameterSetSize + " records are provided before execution. Parameters collection contains: " + this.Parameters.Count + " records.");

            if (this.Parameters.Count > this.MaxParameterSetSize)
                throw new InvalidOperationException("Command requires that at most " + this.MaxParameterSetSize + " records are provided. Parameters collection contains: " + this.Parameters.Count + " records.");

            if (this.RequiredParameters.Length > 0)
            {
                foreach (IDictionary<string, object> record in this.Parameters)
                {
                    foreach (string parName in this.RequiredParameters)
                    {
                        if (!record.ContainsKey(parName))
                            throw new InvalidOperationException("Parameter '" + parName + "' is required but it was not provided.");
                    }
                }
            }
        }

        #endregion

        #region GrantExecute.

        /// <summary>
        /// Checks whether the actor has permissions to execute the command with the provided <b>Parameters</b>.
        /// </summary>
        /// <param name="actor">Actor whose permissions are to be checked.</param>
        /// <returns>Collection of permissions (broken rules) that the actor has failed.</returns>
        public override BusinessRuleCollection GrantExecute(IActor actor)
        {
            EnsureParameters(true);
            ValidateParameters();
            BusinessRuleCollection brokenRules = base.GrantExecute(actor);
            if (brokenRules.AllRulesAreObeyed && this.grantExecuteMethod != null)
            {
                foreach (IDictionary<string, object> paramsAndValues in this.Parameters)
                {
                    TBizObject bizObject = BuildObject(paramsAndValues);
                    brokenRules.Add((BusinessRuleCollection)ReflectionUtil.InvokeMethod(bizObject, this.grantExecuteMethod, new object[] { actor }));
                    if (brokenRules.HasBrokenRules)
                        break;
                }
            }

            return brokenRules.GetBrokenRules();
        }

        #endregion

        #region Execute.

        /// <summary>
        /// Executes the command with provided parameters.
        /// </summary>
        /// <param name="caller">Object/client which is calling the method.</param>
        /// <param name="actor">Command initiator. This may be an application user, software module, 3rd party 
        /// application etc. which is initiating the command.</param>
        /// <remarks>This overload should be used when the command should perform additional security
        /// checks (vertical/horizontal) or when the command execution must be logged. Preffer using
        /// this overload whenever the <see cref="IActor"/> object is available.</remarks>
        public override void Execute(object caller, IActor actor)
        {
            EnsureParameters(true);
            ValidateParameters();
            foreach (IDictionary<string, object> paramsAndValues in this.Parameters)
            {
                TBizObject bizObject = BuildObject(paramsAndValues);
                object[] methodArgs = new object[this.additionalExecuteArguments.Count + 1];
                methodArgs[0] = actor;
                if (this.additionalExecuteArguments.Count > 0)
                    this.additionalExecuteArguments.CopyTo(methodArgs, 1);

                ReflectionUtil.InvokeMethod(bizObject, this.executeMethod, methodArgs);
            }

            if (this.RefreshSearchView)
            {
                ISearchView view = caller as ISearchView;
                if (view != null)
                    view.Search();
            }
        }

        #endregion

        #region Clone, CopyFrom.

        /// <summary>
        /// Creates a new object that is a copy of the current instance. 
        /// </summary>
        /// <remarks>Override the <see cref="CopyFrom"/> method in derived classes if new class fields/members are introduced.</remarks>
        public override object Clone()
        {
            if (GetType() != typeof(BusinessObjectFactoryCommand<TBizObject, TFactory>))
                throw new InvalidOperationException("Clone method must be overriden in derived classes. Do not use \"base.Clone()\" in derived class.");

            BusinessObjectFactoryCommand<TBizObject, TFactory> clone = new BusinessObjectFactoryCommand<TBizObject, TFactory>();
            clone.CopyFrom(this);
            return clone;
        }

        /// <summary>
        /// Copies information from an <b>BusinessObjectFactoryCommand</b> object. 
        /// </summary>
        /// <param name="source">The <b>BusinessObjectFactoryCommand</b> object to base the new object on.</param>
        /// <remarks>This method copies information to an BusinessObjectFactoryCommand object contained in the specified 
        /// BusinessObjectFactoryCommand object. Override in derived classes if new class fields/members are introduced.</remarks>
        protected virtual void CopyFrom(BusinessObjectFactoryCommand<TBizObject, TFactory> source)
        {
            base.CopyFrom(source);
            this.executeMethod = source.executeMethod;
            this.additionalExecuteArguments = (ArrayList)source.additionalExecuteArguments.Clone();
            this.grantExecuteMethod = source.grantExecuteMethod;
            this.parameterToPropertyMappings = new Dictionary<string, string>();
            foreach (KeyValuePair<string, string> parAndProperty in source.parameterToPropertyMappings)
                this.parameterToPropertyMappings.Add(parAndProperty.Key, parAndProperty.Value);

            this.refreshSearchView = source.refreshSearchView;
            this.messageSourceCultureProvider = source.messageSourceCultureProvider;
        }


        #endregion
    }
}
